THE VIRTUAL COLUMN BY RICHARD HALE SHAW GETTING A HANDLE ON DDE Windows 3.1 includes DDEML.DLL, a set of routines for creating and managing DDE conversations. This column explores the library and presents C++ objects that make DDE simpler and easier than ever. This column explores the use of C++ as a Windows development language. It does so in two ways: first, C++ as *the* development language for Windows, largely usurping the role of C; and second, C++ as an object-oriented solution to a non-object-oriented environment. Let me explain. First, I believe that C will no longer be the dominant programming language for Windows developers. I think a number of developers will find that they can solve their Windows programming problems with Visual Basic, Turbo Pascal for Windows or environments like Actor, ToolBook or SmallTalk. But I think that most Windows programmers who currently use C will move to C++ over the next 18 months. Second, Windows -- by any classical definition of OOP -- is *not* an OOP environment. Sure: you can use a window to tie data and code (a window procedure) together, and there's simple (primitive?) sub-classing. But data encapsulation is relatively poor compared to other OOP solutions like C++. Plus, when it comes to inheritance and polymorphism, forget it: Windows doesn't even come close to offering capabilities like these. C++, however, offers these OOP features along with the power of C, and, I believe, is the best tool for creating new Windows programs. Consequently, this column will explore Windows programming from a C++ perspective, and we'll use C++ to create unique solutions to Windows programming problems. As we proceed, don't hesitate to send me your thoughts on these issues or on specific ideas that the column addresses; I'll be sure to share them with everyone as space permits. With this in mind, let's look at the first column: on Windows DDE. If you've ever written a program that uses Windows' Dynamic Data Exchange (DDE), or even attempted to, you know how trying and frustrating it is. DDE -- or as my friend and colleague Charles Petzold calls it, "the protocol from Hell" -- is one of the more exasperating facilities in Windows. The description of DDE in the Windows SDK leaves more to the imagination than to fact, and the only way to be sure your DDE program worked properly has been to test it against Microsoft products that implemented it (after all, we all figure that those guys will get it right). Unfortunately, this isn't always the case, either. But many of us have tried, if desparately, to implement DDE correctly, because the benefit to an application are so worthwhile. Fortunately, there's hope. Windows 3.1 now includes DDEML.DLL: a DLL that packages a number of routines for creating and managing DDE conversations. This DDL makes DDE infinitely simpler to implement in your programs, and reduces the possibility of do so incorrectly. Plus, you can even use DDEML.DLL under Windows 3.0. This article provides an overview of the Windows 3.1 DDEML.DLL and shows you how to use it in your programs. If you've used DDE "in the raw" before, you'll have some 'unlearning' to do first. If you've never programmed DDE before you're in for a treat: with the DDEML, DDE has never been easier. We'll begin with a brief overview of what DDE is and how it works. Then, I'll present you with highlights of the DDE Management library, its features and how it works. Finally, I'll conclude with some C++ objects that utilize the DDEML. Keep in mind that Windows 3.1 was still in beta as this column was written, and the only documentation on the DDEML is in an on-line file. Further, version of Borland C++ that I'm using works appears to work with Windows 3.1, but it doesn't do so officially. The consequence of all this is: there's no absolute guarantee that the code examples will work without modification once Windows 3.1 officially arrives. Fortunately, you'll be able to download an updated version of the code from WTJ at that time. An Overview of Traditional DDE As you're probably aware, DDE lets applications share data. Like the Clipboard, DDE operations are often initiated by the user via an application menu and can transfer data on a one- time basis. But unlike the Clipboard, DDE often continues without user interaction, and it can create on-going data transfers. In DDE terminology, the application that initiates the transfer and requests the data is called the 'client' and the application that provides the data is the 'server'. The client can initiate a 'conversation' (i.e., establish a relationship) with a client and request data from it. DDE clients and servers 'converse' on specific contexts or 'topics' from which smaller data 'items' are exchanged. A topic is usually the name of a file or a document, a database table, a worksheet or an application-specific string. An item is a specific block of text, a database row(s), a cell(s) or another application-specific string. A conversation is uniquely identified by combining the server name and the topic name. Data items are distinguished by the server, topic and item names. A DDE application can simultaneously engage in multiple conversations with multiple DDE applications, on the same or on multiple topics. An application can be a client in one conversation and simultanelusly a server in another. A server can supply data to more than one client at a time and a client can request data from multiple servers at the same time. Thus, 'AppA' can be a client of 'AppB' and 'AppC', and simultaneously be a server to both 'AppB' and 'AppC'. The only hitch is that you keep it all straight. Traditionally speaking, DDE is a protocol: it establishes a set of rules that determine what is supposed to happen, and when. Unfortunately, some of these rules aren't terribly well defined in the WinSDK (in fact, the DDEML docs are *not* monuments to great clarity!), and consequently, it's been difficult to figure out what a DDE application is supposed to do in many circumstances. But the essence of a DDE implementation is that a number of Windows messages are provided, and these could be used to implement the protocol. Since Windows doesn't offer any interprocess communication features in the mature sense (i.e., shared memory, queues, pipes, etc. as will be found in Windows NT), DDE applications use messages (prefixed with 'WM_DDE_') to notify other applications of their intentions, and then transfer data via the global atom table. This table is a collection of strings that any Windows application can register with the system, and which any Windows application can access. Thus to transfer data, a server application puts the data into the atom table, obtains a handle to the data, includes the handle as a parameter with the appropriate DDE message, and sends the latter to the client. The receiving application processes the message and uses the included handle to retrieve the data from the atom table. Finally, since every DDE conversation takes place between two windows (identified in the conversation by their window handles) most Windows developers dedicate an invisible window to every DDE conversation. This is particularly helpful if an application engages in more than one conversation at a time. This requires creating the new window and writing a window procedure that can respond to the appropriate DDE messages. (Interestingly, many Windows developers call these 'object windows' and did so long before the Borland application framework of the same name appeared.) The DDE Managment Library Under the hood, the DDEML still uses the approach outlined above: it communicates with DDE client and server applications via the standard WM_DDE messages found in Windows. And, it transfers data between those applications by using the global atom table. For these reasons, an application that uses DDEML will be completely compatible with DDE applications written without the benefit of DDEML. However, DDEML applications needn't process WM_DDE_* messages or access the atom table. Instead, the DDEML provides a set of API functions, along with a few structures and special messages known as 'transactions'. These facilities serve to hide the details of traditional DDE conversations. So, your application need only make a few function calls, and be designed to handle the transaction messages when they are received. While DDEML transactions are messages, they're not sent to your application's window procedure. Instead, you must supply a special function of your own that, like a window procedure, is called by DDEML whenever your application receives a transaction. This 'callback function' (or simply 'callback' for short) must be capable of processing any transactions that your application receives from DDEML. In addition, it must be able to respond by posting transactions as replies and (if your application is a DDE server) by supplying the appropriate data. One of the nicer features of the DDEML is that you can filter out unwanted and unneeded transactions, thus limiting the number and types of transactions that have to be handled by your application. These 'transaction filters' are completely dynamic: you can change them on-the-fly -- even in the midst of a DDE conversation. The consequence of this feature is that you can optimize the performance of your DDE application: your callback will never even receive the filtered messages, and the sending application will receive a reply transaction indicating that the transaction it sent wasn't processed (unless the sending application itself filters out *these* transactions). The DDEML lets you create DDE conversations that utilize both synchronous *and* asynchronous transactions. And, the DDEML supports all the traditional DDE conversational forms: Request, Advise with notification (a.k.a., 'Warm Link'), Advise with Data (a.k.a. 'Hot Link'), Execute and Poke. (In talks I've given at SoftWare Development, I've had more than one student point out the implicit sexual metaphor in the choice of these terms -- but they're all legitimate, i.e., chosen by Microsoft.) In addition, you can use the DDEML to create DDE Monitor, i.e., an application that can monitor all the DDE conversations taking place in the system. An example of just such a program is DDESPY.EXE, which comes with the Windows 3.1 SDK. Initializing DDEML The DDEML functions fall into several categories. For instance, there are functions for initializing the connection between your application and the DDEML, and for establishing (or terminating) DDE conversations. There are transaction management functions for initiating a transaction and abandoning asynchronous ones. Other API functions let you create global data objects, copy data to them and free them. The entire DDEML API is summarized in the table in Figure 1. You must #include DDEML.H in the source files that reference them and link in DDEML.LIB (DDEML.LIB is the import library for DDEML.DLL -- which should be on the PATH when you run the application). Before it can call any other DDEML function, you must register your application with the DDEML by calling DdeInitialize: DWORD idInst = 0L; FARPROC lpDdeProc; WORD errval; . . .. lpDdeProc = MakeProcInstance((FARPROC) DDECallback, hInst); if(DdeInitialize((LPDWORD) &idInst,(PFNCALLBACK)lpDdeProc, APPCMD_CLIENTONLY, 0L)) return FALSE; DdeInitialize takes a pointer to a DWORD as its first parameter, and sets this to an 'application-instance identifier' -- essentially a handle that refers to that instance of the task's usage of the DDEML (a task can have more than one instance of DDEML). Without this handle, the DDEML wouldn't be able to distinguish one usage of DDEML.DLL from another by the same task, a problem if the application needs to use the DDEML more than once at the same time. For instance, suppose an application is using OLE (which uses DDE to link and embed objects) while it's using the DDEML. The instance identifier lets the DDEML keep the application's use of DDE in the OLE DLL distinct from the application's own use of the DDEML. Consequently, you have to retain the instance handle when DdeInitialize returns, and pass it to a great number of the DDEML API functions. It's via DdeInitialize that you provide the address of your application's callback (in the 2nd parameter). If your application needs to establish DDE conversations with different behaviors, you can call DdeInitialize for each type of conversation, passing it a different callback function address, and receiving a different instance handle each time. Each instance handle is tied to the callback function passed with each call to DdeInitialize. You can also use DdeInitialize to set transaction filters (and prevent the receipt of a variety of unwanted messages), as well as specify whether the application is a DDE monitor and or needs other services from the DDEML. Server Naming Services Once an application has registered itself with DDEML, it can either initiate a conversation (if it's a DDE client) or register its name and the topics it supports (if it's a DDE server). In traditional DDE, the name of a DDE server is based on the application name. For example, the server name of EXCEL.EXE is 'Excel'. To access Excel as a server with traditional DDE, you have to specify that name (or rather an atom handle for it) in the WM_DDE_INITIATE sent by your client application. (Of course, a client application can always use wildcards invoke reponses from *all* the available servers.) Plus, there's no way to know if Excel is available without first attempting to converse with it. With DDEML, a server can take on more than one name: the DdeNameService function lets a DDE server register multiple names with the system. This function will send an XTYPE_REGISTER transaction to every DDEML application in the system (except those that are filtering out this transaction), thus notifying them that the new server is available. One advantage is that client applications won't have to attempt to converse with a server to find out if it's available: they'll be notified once it's available. Another advantage is that the server can be more efficient: DdeNameService can toggle the server's filtering of XTYPE_CONNECT transactions (which are sent to every server when a client is trying to start a conversation -- regardless of the server name specified by the client). Thus, after calling DdeNameService, the server will only receive XTYPE_CONNECT transactions when a client specifies one of the registered server names. A server can also use DdeNameService to *de-register* a server name: since this causes DDEML to send the XTYP_UNREGISTER transaction to every DDEML client, client applications will know when a server is no longer available. A server can also use DdeNameService to facilitate aliasing: the server can use different names to indicate different types of data as it becomes available. Creating A Conversation A client application can establish a DDE conversation by calling DdeConnect: DWORD idInst; HSZ hszServer; HSZ hszTopic; HCONV hConv; . . . hConv = DdeConnect(idInst,hszServer, hszTopic,(LPVOID)NULL); This function sends an XTYP_CONNECT transaction to the specified server and takes four parameters: the instance handle returned from DdeInitialize, and two string handles (one for the server name and the other for the topic name). Either or both of the string handles can be NULL: as with traditional DDE, a NULL server name handle will allow a connection with any available server, and a NULL topic handle will allow a conversation on any topic supported by the selected server. The final parameter is a pointer to a CONVCONTEXT structure (defined in DDEML.H). You can pass a NULL instead, and the server will receive a default CONVCONTEXT structure along with the XTYP_CONNECT transaction. The string handles are created by calling DdeCreateStringHandle. In traditional DDE, you have to create an invisible window that's dedicated to managing the DDE conversation for you. If DdeConnect finds a server with the specified name that supports the specified topic, it will return a conversation handle: in this implementation of DDEML, a handle to an invisible window which DDEML creates for you. All messages (even WM_DDE_* messages) sent to this window are handled by DDEML (it supplies the window's window procedure) and then passed on to your application's callback as DDEML transactions. Since there's no guarantee that this approach will be used in future implementations of DDEML, you can't rely on the conversation handle to be a window handle. But you can still use the conversation handle returned by DdeConnect to refer to a particular conversation in other calls to the DDEML. And it's vital for distinguishing between conversations if your application is maintaining more than one DDEML conversation at a time. Note that the DDEML also provides DdeConnectList, which lets a client application establish more than one conversation at a time. And, an application can terminate either single or multiple conversations with DdeDisconnect or DdeDisconnectList, respectively. Once an conversation has been established between a client and a server, the client application can call DdeClientTransaction to receive data from the server: HCONV hConv; HSZ hszItem; HDDEDATA hData; DWORD dwResult; . . . hData = DdeClientTransaction((LPBYTE)NULL,0,hConv,hszItem, CF_TEXT,XTYP_ADVSTART,1000L,&dwResult); This function lets a client application receive data on a one-time basis (cold-link), receive notification of updates (warm-link) or receive the updated data directly (hot-link). The first two parameters are supplied in the event that the client wants to pass data to the server (for the XTYP_EXECUTE or XTYPE_POKE transactions), and specify a pointer to the data and the data length. The third parameter is the conversation handle, and the following one is a string handle that specifies the data item being requested (like other string handles, created via DdeCreateStringHandle). The fifth parameter specifies the Clipboard data format being used (such as CF_TEXT). The next parameter to DdeClientTransaction is the transaction type, which can be: XTYP_ADVSTART (to initiate a warm or hot link), XTYP_ADVSTOP (to terminate such a link), XTYP_EXECUTE (to issue commands to the server), XTYP_POKE (to offer data to the server) and XTYP_REQUEST (to make a one-time request for data). The remaining two parameters specify a timeout value (the maximum time in milliseconds that a client will wait for a response from the server) and a result-flag variable. The bits of the latter are set to designate the result of the transaction. If it's successful, DdeClientTransaction returns a handle to a global memory object that contains the data sent from the server (provided the transaction was one that requested data). Your application can pass this HDDEDATA handle to DdeGetData to retrieve the data: HDDEDATA hData; char szBuf[100]; . . . DdeGetData(hData, (LPBYTE)szBuf, 100L, 0L); In this example, DdeGetData will retrieve the data from a global memory object (represented by hData) and place it in szBuf. The last two parameters specify the number of bytes to copy from the global object to the buffer and at what offset to begin copying. If you don't know how big the data item is, you can pass a NULL in place of the local buffer (in this case, szBuf), and DdeGetData will return the size on bytes of the data item. If this all sounds terribly complicated, it's not. The listings accompanying this article show how to use these DDEML functions in the context of a C++ object that makes a one-time data request from a DDE server. CallBack Functions While the examples above didn't necessitate a callback on the client side, they don't preclude the client from having one and the server certainly needed one. If you haven't already guessed, callbacks are a *lot* like window procedures. Both consist of a large *switch* statement, process messages, and are called by Windows, not your application. DDEML callbacks differ from window procedures only in the number and types of their arguments and the messages they process. Here, for example, is a skeleton callback function: HDDEDATA EXPENTRY DdeProc(WORD usType,WORD usFmt,HCONV hConv, HSZ hsz1,HSZ hsz2,HDDEDATA hData,DWORD lData1,DWORD lData2) { . . . switch(usType) { . . . } } As you can see, there are eight arguments passed to a callback by the DDEML. (It's too bad that Microsoft didn't just supply a pointer to a structure and pass the structure address to the callback -- it would have been simpler, easier and faster). The transaction message is the first parameter, followed by the data format and the conversation handle. Whether the remaining parameters (two string handles, a data handle and two transaction-specific double-words) are used depends on the transaction that's passed. Synchronous vs. Asynchronous Transactions At this point, you should have a general idea of how DDEML works. Before I present some C++ code examples for using it in an application, there's on other feature of DDEML that's well worth mentioning. DDEML supports both asychronous and synchronous transactions. The client can conform to the model of traditional DDE and issue synchronous transactions where the client waits for the transaction to be processed (as shown in the example using DdeClientTransaction shown above). Or, it can issue an asynchronous transactions: the client returns immediately after calling DdeClientTransaction and proceeds normally; when the server replies, the client's callback will receive the response transaction. Synchronous transactions are simpler, faster and easier to use. But they can hold up the client if the server takes a long time to process a transaction or is processing a high volume of transactions for this and other clients. Asynchronous transactions, while more complex and difficult to maintain, allow the client more control while a transaction is in progress; indeed, a client and go on to issue other transactions to the same or other servers. Using C++ with DDEML To give you a 'taste' of what DDEML will be like (indeed, it should be available and fully documented not long after this article appears), I've written a few C++ classes that use it. (Keep in mind that these won't be fully tested until after DDEML is released, and that these classes should be considered 'works in progress'. But they should give you a better idea of how DDEML can be used from a C++ application.) The two classes, DDE and DDEClient, can be found in DDE.H and DDE.CPP. Class DDE contains all the basic facilities that are needed by a DDE conversation, including the instance handle and a pointer to the callback function (which an application that uses the class has to provide). DDEClient relies on the DDE class for these, and also provides member function to connect to a DDE server and make a request for data. While I didn't have time to implemenent member functions that can set up advise loops (hot and warm links), it'd be simple enough to add them using the facilities already built into DDEClient. Both classes compile under Borland C++ 2.0 -- the latest Borland compiler available at the time this column was written. (Borland C++ 3.0 was still in Beta, and Microsoft C 7.0 had not reached beta.) Also, both DDEClient and the DDE class rely on some application classes that I wrote and have been using over the past year. However, you can easily reconfigure both of these classes to work with an application class like Borland's ObjectWindows. To demonstrate these DDE classes, I wrote a program, DDECLNT.EXE, which uses DDEML to connect to any DDE server and retrieve data from it. For example, to connect to Excel and retrieve rows 1-3, columns 1-4 from ANNUAL.XLS (a worksheet that comes with Excel): o Load Excel o Use File|Load to load ANNUAL.XLS o Load DDECLNT o In DDECLNT's window (a single modal dialog), enter "Excel" in the 'Server Name' field, "ANNUAL.XLS" in the 'Topic Name' field, and "r1c1:r3c4" in the 'Item Name' field. o Press the 'Request' button. This will cause DDECLNT to create a DDEClient object, DDEClient newClient(Bufs[0].buffer, Bufs[1].buffer, (FARPROC)DDEClientCallBack); and pass it the server name and topic name as the first two parameters, with the address of a callback function as the last parameter. To make a data request, DDECLNT.EXE only needs to call the Request member function: newClient.Request(Bufs[2].buffer); Then, if a call to newClient.GetResult returns TRUE, the program retrieves the data from newClient with a call to its GetData member function, SetDlgItemText(hDlg,RESULT_DATA,(LPSTR)newClient.GetData()); and puts the data directly into one of the data fields in the main program's dialog. The DDE and DDEClient classes do all the work. I've tested the program under Win3.0 and Win3.1. I can't guarantee it until Win3.1 and the final version of DDEML.DLL are available, but you shouldn't have any problems with it. By the way: considering how much DDECLNT.EXE does, I'm surprised that it's only a little over 10k (of course, the 36k DDEML.DLL has to be available, too). Conclusion As you can see, the DDEML is a powerful addition to Windows 3.1, but there's a lot to it. C++ certainly makes DDEML easier to deal with. I'd like to re-visit DDE again in future columns, once Win3.1 arrives and these classes have time to mature. But, until next month, enjoy! =========================================== [figure 1] Session Management ------------------------ DdeInitialize Registers an application with the DDEML DdeUninitialize Frees an application's DDEML resources Conversation Management ------------------------ DdeConnect Establishes a conversation with a server DdeConnectList Establishes multiple DDE conversations DdeDisconnect Terminates a DDE conversation DdeDisconnectList Destroys a DDE conversation list DdeEnableCallback Enables or disables one or more DDE conversations DdePostAdvise Prompts a server to send advise data to a client DdeQueryConvInfo Retrieves information about a DDE conversation DdeQueryNextServer Obtains the next handle in a conversation list Transaction Management ------------------------ DdeAbandonTransaction Abandons an asynchronous transaction DdeClientTransaction Begins a DDE data transaction Data Management ------------------------ DdeAccessData Accesses a DDE global memory object DdeAddData Adds data to a DDE global memory object DdeCreateDataHandle Creates a DDE data handle DdeFreeDataHandle Frees a global memory object DdeGetData Copies data from a global memory object to a buffer DdeUnaccessData Frees a DDE global memory object String Management ------------------------ DdeCmpStringHandles Compares two DDE string handles DdeCreateStringHandle Creates a DDE string handle DdeFreeStringHandle Frees a DDE string handle DdeKeepStringHandle Increments the use count for a string handle DdeQueryString Copies string-handle text to a buffer Miscellaneous Functions ------------------------ DdeGetLastError Returns the error code set by a DDEML function DdeNameService Registers or unregisters server name(s) DdeSetUserHandle Associates a user-defined handle with a transaction =================================================================